package com.zxl.demo.annotation;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;

/**
 * 对自定义注解的处理
 */
@Aspect
@Component
public class OperateCustomAnnotation {

    private static Logger logger = LoggerFactory.getLogger(OperateCustomAnnotation.class);

    /**
     * 全局的logId属性名称,如果配置文件中没有设置logId.name的值，那么就设置为默认值logId
     */
    @Value("${logId.name:logId}")
    private String logIdPropertity;

    /**
     * 局部的logId属性名称
     */
    private String localLogIdProp;

    /**
     * 切入点，前置通知时设置其值
     */
    private JoinPoint joinPoint;

    /**
     * logId
     */
    private String logId;

    /**
     * 设置切入点为打上了我们自己的注解@CustomAnnotation的方法
     */
    @Pointcut("@annotation(com.zxl.demo.annotation.CustomAnnotation) ")
    public void pointCut() {
    }


    /**
     * 前置: 切入的方法执行之前执行
     *
     * @param joinPoint
     */
    @Before("pointCut()")
    public void before(JoinPoint joinPoint) throws IllegalAccessException {
        this.joinPoint = joinPoint;
        // 判断是否指定了局部日志id的属性名称
        setLogIdPropertity(joinPoint);
        // 获取日志id
        if (setLogId(joinPoint)) ;
        {
            outLog(1, null, null);
        }
        outLog(3, null, new Exception("get logId fail!"));
    }

    /**
     * 根据注解中指定的属性名称来设置logId
     * @param joinPoint
     */
    private void setLogIdPropertity(JoinPoint joinPoint) {
        MethodSignature sign =getSignByJoinPoint(joinPoint);
        CustomAnnotation customAnnotation=sign.getMethod().getAnnotation(CustomAnnotation.class);
        if(!StringUtils.isEmpty(customAnnotation.logIdPropertity())){
            this.localLogIdProp=customAnnotation.logIdPropertity();
        }else {
            this.localLogIdProp=this.logIdPropertity;
        }
    }

    private MethodSignature getSignByJoinPoint(JoinPoint joinPoint){
        return (MethodSignature) joinPoint.getSignature();
    }

    /**
     * 获取日志id,日志是由业务处理流程的开始处生成后，一直随着业务流程在整个系统内部传递。
     * 查询logId的属性值的顺序：
     *          1.切入点方法的形参列表中的指定参数
     *          2.形参列表中参数类中
     *          3.形参列表中的参数类的父类中
     *
     * 注意：logId的取值只会取第一个匹配到的数据，请不要使用会重复重新的属性名称最为logId,默认的logId取值的属性名称为 "logId"
     *
     * @param joinPoint
     * @return
     */
    private boolean setLogId(JoinPoint joinPoint) throws IllegalAccessException {
        Object[] pointArgs = joinPoint.getArgs();
        MethodSignature sign =getSignByJoinPoint(joinPoint);
        String[] parameterNames = sign.getParameterNames(); // 获取目标方法的参数名称
        for (int i = 0; i < pointArgs.length; i++) {
            // 设置this.logId为形参中的同名参数的值
            if (!setLogIdByParamName(parameterNames[i], pointArgs[i])) {
                // 参数列表中没有为logId的参数，那么查询参数列表中所有的参数的属性
                setLogIdByParamProperty(pointArgs[i]);
            }
        }
        if (StringUtils.isEmpty(this.logId)) {
            return false;
        }
        return true;
    }

    /**
     * 根据参数名称设置logId
     *
     * @param parameterName
     * @param pointArg
     * @return
     */
    private boolean setLogIdByParamName(String parameterName, Object pointArg) {
        if (parameterName.equals(localLogIdProp)) {
            this.logId = (String) pointArg;
            return true;
        }
        return false;
    }

    /**
     * @param retVal
     */
    @AfterReturning(returning = "retVal", pointcut = "pointCut()")
    public void after(Object retVal) {
        outLog(2, retVal, null);
    }


    /**
     * 方法执行时发生异常后 ,
     * 注意：@AfterThrowing  不会完全处理异常，这个异常最终会抛到jvm。
     *
     * @param ex
     */
    @AfterThrowing(pointcut = "pointCut()", throwing = "ex")
    public void throwing(Exception ex) {
        outLog(3, null, ex);
    }


    /**
     * 输出日志
     *
     * @param stage  日志输出的阶段， 1- 开始执行；2-执行结束; 3- 发生异常
     * @param retVel 方法的返回值，只有stage为2时才需要填写
     */
    private void outLog(int stage, Object retVel, Exception ex) {
        MethodSignature sign = (MethodSignature) joinPoint.getSignature();  // 注解的签名
        Method method = sign.getMethod();  // 从签名中获取方法
        String itfName = method.getAnnotation(CustomAnnotation.class).itfName(); // 获取注解类里的变量itfName
        if (stage == 1) {
            StringBuffer strBuffer = new StringBuffer();
            Object[] params = joinPoint.getArgs();
            StringBuffer paramType = new StringBuffer();

            // 获取参数的类型
            Arrays.stream(params)
                    .forEach(t -> paramType.append(t.getClass().toString()).append(","));

            // 获取参数的值
            Arrays.stream(params)
                    .forEach(e -> strBuffer.append(e.toString()).append(","));

            logger.info("id:[{}],类名:[{}]，方法名:[{}]，接口:[{}]，状态:[ 开始执行 ]，参数类型:[{}]，参数:[{}],返回值类型:[{}]",
                    this.logId, method.getDeclaringClass(), method.getName(), itfName, paramType, strBuffer, sign.getReturnType());
        } else if (stage == 2) {
            logger.info("id:[{}],类名:[{}]，方法名:[{}]，接口:[{}]，状态:[ 执行完成 ]，返回值类型:[{}],返回值:[{}]",
                    this.logId, method.getDeclaringClass(), method.getName(), itfName, sign.getReturnType(), retVel);
        } else if (stage == 3) {
            logger.error("id:[{}],类名:[{}]，方法名:[{}]，接口:[{}]，状态:[ 发生异常 ]，异常:[{}]",
                    this.logId, method.getDeclaringClass(), method.getName(), itfName, ex.toString());
        }

    }

    /**
     * 通过参数的属性来设置logId
     *
     * @param param
     */
    public void setLogIdByParamProperty(Object param) {
        setLogIdByClass(param.getClass(), param);
    }


    private boolean setLogIdByClass(Class<?> clazz, Object obj) {
        // 通过getDeclaredFields() 获取field (获取所有字段,public和protected和private,但是不包括父类字段)
        Field[] fields = clazz.getDeclaredFields();
        boolean result = setLogIdByFields(fields, obj);
        if (!result) {
            //  通过getFields() 获取field （获取所有public字段,包括父类字段）
            fields = clazz.getFields();
            result = setLogIdByFields(fields, obj);
        }
        return result;
    }

    private boolean setLogIdByFields(Field[] fields, Object obj) {
        Optional<Field> fieldOptional = Arrays.stream(fields)
                .filter(f -> f.getName().equals(localLogIdProp)).findFirst();
        if (fieldOptional.isPresent()) {
            return setLogIdByField(fieldOptional.get(), obj);
        }
        return false;
    }

    private boolean setLogIdByField(Field field, Object obj) {

        if (field == null || obj == null) {
            return false;
        }

        field.setAccessible(true);
        try {
            this.logId = (String) field.get(obj);
            return true;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return false;
    }


}
